
import { Mat3 } from "./Mat3";
import { MathUtil } from "./MathUtil";
import { Mat4 } from "./Mat4";
import { Transform } from "./Transform";

/**
 * 3D vector class.
 */
export class Vec3 {
	/**
	 * The number of instance creation.
	 */
	public static numCreations: number = 0;

	/**
	 * The x-value of the vector.
	 */
	x: number;

	/**
	 * The y-value of the vector.
	 */
	y: number;

	/**
	 * The z-value of the vector.
	 */
	z: number;

	/**
	 * Creates a new vector. The vector is zero vector by default.
	 */
	constructor(x: number = 0, y: number = 0, z: number = 0) {
		this.x = x;
		this.y = y;
		this.z = z;
		Vec3.numCreations++;
	}

	/**
	 * Sets all values at once and returns `this`.
	 */
	public init(x: number, y: number, z: number): Vec3 {
		this.x = x;
		this.y = y;
		this.z = z;
		return this;
	}

	public isZero(): boolean {
		return this.x == 0 && this.y == 0 && this.z == 0;
	}

	/**
	 * Sets this vector to zero vector and returns `this`.
	 */
	public zero(): Vec3 {
		return this.initi(0, 0, 0);
	}

	public abs(): Vec3 {
		return new Vec3(
			Math.abs(this.x),
			Math.abs(this.y),
			Math.abs(this.z)
		);
	}

	public absTo(to: Vec3) {
		to.init(
			Math.abs(this.x),
			Math.abs(this.y),
			Math.abs(this.z)
		);
	}

	public absEq() {
		this.x = Math.abs(this.x);
		this.y = Math.abs(this.y);
		this.z = Math.abs(this.z);
	}

	public clampTo(min: Vec3, max: Vec3, to: Vec3) {
		to.initi(
			MathUtil.clamp(this.x, min.x, max.x),
			MathUtil.clamp(this.y, min.y, max.y),
			MathUtil.clamp(this.z, min.z, max.z),
		);
	}

	private initi(x: number, y: number, z: number): Vec3 {
		this.x = x;
		this.y = y;
		this.z = z;
		return this;
	}

	/**
	 * Returns `this` + `v`.
	 */
	public add(v: Vec3): Vec3 {
		return new Vec3(this.x + v.x, this.y + v.y, this.z + v.z);
	}

	/**
	 * Returns `this` + `v`. no clone
	 */
	public addTo(v: Vec3, to: Vec3): Vec3 {
		return to.initi(this.x + v.x, this.y + v.y, this.z + v.z);
	}

	/**
	 * Returns `this` + `v` * `s`.
	 */
	public addScaled(v: Vec3, s: number): Vec3 {
		return new Vec3(this.x + v.x * s, this.y + v.y * s, this.z + v.z * s);
	}

	/**
	 * Returns `this` + `v` * `s`. no clone
	 */
	public addScaledTo(v: Vec3, s: number, to: Vec3): Vec3 {
		return to.initi(this.x + v.x * s, this.y + v.y * s, this.z + v.z * s);
	}

	/**
	 * Returns `this` - `v`.
	 */
	public sub(v: Vec3): Vec3 {
		return new Vec3(this.x - v.x, this.y - v.y, this.z - v.z);
	}

	/**
	 * Returns `this` - `v`. No Clone
	 */
	public subTo(v: Vec3, to: Vec3) {
		to.initi(this.x - v.x, this.y - v.y, this.z - v.z);
	}

	/**
	 * Returns `this` * `s`.
	 */
	public scale(s: number): Vec3 {
		return new Vec3(this.x * s, this.y * s, this.z * s);
	}

	/**
	 * Returns `this` * `s`. no clone
	 */
	public scaleTo(s: number, to: Vec3): Vec3 {
		return to.initi(this.x * s, this.y * s, this.z * s);
	}

	/**
	 * Returns (`this.x` * `sx`, `this.y` * `sy`, `this.z` * `sz`).
	 */
	public scale3(sx: number, sy: number, sz: number): Vec3 {
		return new Vec3(this.x * sx, this.y * sy, this.z * sz);
	}

	/**
	 * Returns the dot product of `this` and `v`.
	 */
	public dot(v: Vec3): number {
		return this.x * v.x + this.y * v.y + this.z * v.z;
	}

	/**
	 * Returns the cross product of `this` and `v`.
	 */
	public cross(v: Vec3): Vec3 {
		return new Vec3(
			this.y * v.z - this.z * v.y,
			this.z * v.x - this.x * v.z,
			this.x * v.y - this.y * v.x
		);
	}

	public crossTo(v: Vec3, to: Vec3) {
		return to.initi(
			this.y * v.z - this.z * v.y,
			this.z * v.x - this.x * v.z,
			this.x * v.y - this.y * v.x
		);
	}

	/**
	 * Sets this vector to `this` + `v` and returns `this`.
	 */
	public addEq(v: Vec3): Vec3 {
		return this.initi(this.x + v.x, this.y + v.y, this.z + v.z);
	}

	/**
	 * Sets this vector to `this` + `v` * `s` and returns `this`.
	 */
	public addScaledEq(v: Vec3, s: number): Vec3 {
		return this.initi(this.x + v.x * s, this.y + v.y * s, this.z + v.z * s);
	}

	/**
	 * Sets this vector to `this` - `v` and returns `this`.
	 */
	public subEq(v: Vec3): Vec3 {
		return this.initi(this.x - v.x, this.y - v.y, this.z - v.z);
	}

	/**
	 * Sets this vector to `this` * `s` and returns `this`.
	 */
	public scaleEq(s: number): Vec3 {
		return this.initi(this.x * s, this.y * s, this.z * s);
	}

	/**
	 * Sets this vector to (`this.x` * `sx`, `this.y` * `sy`, `this.z` * `sz`) and returns `this`.
	 */
	public scale3Eq(sx: number, sy: number, sz: number): Vec3 {
		return this.initi(this.x * sx, this.y * sy, this.z * sz);
	}

	/**
	 * Sets this vector to the cross product of `this` and `s`, and returns `this`.
	 */
	public crossEq(v: Vec3): Vec3 {
		return this.initi(
			this.y * v.z - this.z * v.y,
			this.z * v.x - this.x * v.z,
			this.x * v.y - this.y * v.x
		);
	}

	public minEq(v: Vec3): Vec3 {
		return this.initi(
			Math.min(v.x, this.x),
			Math.min(v.y, this.y),
			Math.min(v.z, this.z),
		);
	}

	public minTo(v: Vec3, to: Vec3) {
		return to.initi(
			Math.min(v.x, this.x),
			Math.min(v.y, this.y),
			Math.min(v.z, this.z),
		);
	}

	public maxEq(v: Vec3): Vec3 {
		return this.initi(
			Math.max(v.x, this.x),
			Math.max(v.y, this.y),
			Math.max(v.z, this.z),
		);
	}

	public maxTo(v: Vec3, to: Vec3) {
		return to.initi(
			Math.max(v.x, this.x),
			Math.max(v.y, this.y),
			Math.max(v.z, this.z),
		);
	}

	/**
	 * Returns the transformed vector by `m`.
	 */
	public mulMat3(m: Mat3): Vec3 {
		return new Vec3(
			this.x * m.e00 + this.y * m.e01 + this.z * m.e02,
			this.x * m.e10 + this.y * m.e11 + this.z * m.e12,
			this.x * m.e20 + this.y * m.e21 + this.z * m.e22
		);
	}

	/**
	 * Returns the transformed vector by `m`. no clone
	 */
	public mulMat3to(m: Mat3, to: Vec3): Vec3 {
		return to.init(
			this.x * m.e00 + this.y * m.e01 + this.z * m.e02,
			this.x * m.e10 + this.y * m.e11 + this.z * m.e12,
			this.x * m.e20 + this.y * m.e21 + this.z * m.e22
		);
	}

	/**
	 * Returns the transformed vector by `m`.
	 */
	public mulMat4(m: Mat4): Vec3 {
		return new Vec3(
			this.x * m.e00 + this.y * m.e01 + this.z * m.e02 + m.e03,
			this.x * m.e10 + this.y * m.e11 + this.z * m.e12 + m.e13,
			this.x * m.e20 + this.y * m.e21 + this.z * m.e22 + m.e23
		);
	}

	/**
	 * Returns the transformed vector by `tf`.
	 */
	public mulTransform(tf: Transform): Vec3 {
		var v: Vec3 = this;
		v = v.mulMat3(tf._rotation);
		v = v.add(tf._position);
		var res: Vec3 = new Vec3();
		res.copyFrom(v);
		return res;
	}

	/**
	 * Sets this vector to the transformed vector by `m` and returns `this`.
	 */
	public mulMat3Eq(m: Mat3): Vec3 {
		return this.initi(
			this.x * m.e00 + this.y * m.e01 + this.z * m.e02,
			this.x * m.e10 + this.y * m.e11 + this.z * m.e12,
			this.x * m.e20 + this.y * m.e21 + this.z * m.e22
		);
	}

	/**
	 * Sets this vector to the transformed vector by `m` and returns `this`.
	 */
	public mulMat4Eq(m: Mat4): Vec3 {
		return this.initi(
			this.x * m.e00 + this.y * m.e01 + this.z * m.e02 + m.e03,
			this.x * m.e10 + this.y * m.e11 + this.z * m.e12 + m.e13,
			this.x * m.e20 + this.y * m.e21 + this.z * m.e22 + m.e23
		);
	}

	/**
	 * Sets this vector to the transformed vector by `tf` and returns `this`.
	 */
	public mulTransformEq(tf: Transform): Vec3 {
		var v: Vec3 = this;
		v = v.mulMat3(tf._rotation);
		v = v.add(tf._position);
		this.copyFrom(v);
		return this;
	}

	/**
	 * Returns the length of the vector.
	 */
	public length(): number {
		return MathUtil.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
	}

	/**
	 * Returns the squared length of the vector.
	 */
	public lengthSq(): number {
		return this.x * this.x + this.y * this.y + this.z * this.z;
	}

	/**
	 * Returns the normalized vector.
	 *
	 * If the length is zero, zero vector is returned.
	 */
	public normalized(): Vec3 {
		var invLen: number = this.length();
		if (invLen > 0) invLen = 1 / invLen;
		return new Vec3(this.x * invLen, this.y * invLen, this.z * invLen);
	}

	/**
	 * Normalize this vector and returns `this`.
	 *
	 * If the length is zero, this vector is set to zero vector.
	 */
	public normalize(): Vec3 {
		var invLen: number = this.length();
		if (invLen > 0) invLen = 1 / invLen;
		return this.initi(this.x * invLen, this.y * invLen, this.z * invLen);
	}

	public normalzeTo(to: Vec3) {
		var invLen: number = this.length();
		if (invLen > 0) invLen = 1 / invLen;
		return to.initi(this.x * invLen, this.y * invLen, this.z * invLen);
	}

	/**
	 * Returns the nagated vector.
	 */
	public negate(): Vec3 {
		return new Vec3(-this.x, -this.y, -this.z);
	}

	/**
	 * Negate the vector and returns `this`.
	 */
	public negateEq(): Vec3 {
		return this.initi(-this.x, -this.y, -this.z);
	}

	public negateTo(to: Vec3) {
		return to.initi(-this.x, -this.y, -this.z);
	}

	/**
	 * Copies values from `v` and returns `this`.
	 */
	public copyFrom(v: Vec3): Vec3 {
		this.x = v.x;
		this.y = v.y;
		this.z = v.z;
		return this;
	}

	/**
	 * Returns a clone of the vector.
	 */
	public clone(): Vec3 {
		return new Vec3(this.x, this.y, this.z);
	}

	/**
	 * Returns the string representation of the vector.
	 */
	public toString(): String {
		return "Vec3[" + this.x.toFixed(2) + ", " + this.y.toFixed(2) + ", " + this.z.toFixed(2) + "]";
	}

}
